Skip to content

fix(rpc): avoid race when reading storage proof state - #4064

Open
cicr99 wants to merge 2 commits into
mainfrom
fix/storage-proof-state-at-block
Open

cicr99 wants to merge 2 commits into
mainfrom
fix/storage-proof-state-at-block

Conversation

@cicr99

@cicr99 cicr99 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

Fixes a race condition in starknet_getStorageProof when resolving latest. (Reference to the comment here)

Before this change, the handler read the current chain height, validated the requested block against that height, and then opened HeadState(). If a new block was accepted between those two operations, the response could use metadata from block n while reading state from block n + 1.

This updates the handler to open state explicitly at the validated block number instead of using the moving head state.

Notes

This is a tactical fix for the existing inconsistency. A longer-term solution would be to use a DB snapshot so the chain height, header/hash lookup, and state access are all served from the same underlying database view.


PR Type

Bug fix, Tests


Description

  • Pin storage proof state requested height

  • Add trie reader by block number

  • Use block-pinned state in storage proof

  • Update mocks and storage proof tests


File Walkthrough

Relevant files

@cicr99
cicr99 requested a review from rodrodros September 11, 2026 11:25
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cicr99's task in 12m 42s —— View job


Review summary

Blocker (1 issue, posted inline on both rpc/v10/storage.go:209 and rpc/v9/storage.go:96):

Swapping HeadState() for h.bcReader.StateAtBlockNumber(chainHeight) fixes the described race, but StateAtBlockNumber returns a stateHistory-backed reader (core/state/history.go) whose ClassTrie(), ContractTrie(), and ContractStorageTrie() are all stubbed to unconditionally return ErrHistoricalTrieNotSupported. StorageProof needs exactly these methods to build the Merkle proofs, so every real (non-mocked) call to starknet_getStorageProof now fails with an internal error.

This isn't theoretical — this PR's own CI run already shows it: Juno Test (run 34593901648) fails on TestStorageProof_StorageRoots/get_contract_proof in both rpc/v9 and rpc/v10, which exercise a real blockchain.Blockchain + synced state rather than a mock. The unit tests added/touched in this PR pass only because they mock bcReader.StateAtBlockNumber to return a MockStateReader that doesn't have this historical-trie restriction, so the mocked tests mask the regression.

The underlying race is legitimate and worth fixing, but the fix needs a state accessor that pins to a specific block's global state root while still supporting trie reconstruction — e.g., resolving the state root at chainHeight directly and building a state.NewStateReader(stateRoot, ...), or extending the history state path to support ClassTrie/ContractTrie/ContractStorageTrie for the head block. As currently written, this PR would need to fail CI and shouldn't be merged in its current form.

@cicr99
cicr99 deployed to Development September 11, 2026 11:33 — with GitHub Actions Active
Comment thread rpc/v10/storage.go Outdated
Comment thread rpc/v9/storage.go Outdated
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Review in progress

  • Gather context, read prior review feedback (the earlier StateAtBlockNumber regression was addressed by adding a new TrieStateAtBlockNumber method)
  • Review blockchain/blockchain.go, blockchain/statebackend/{statebackend,deprecated,types}.go
  • Review rpc/v9/storage.go, rpc/v10/storage.go handler changes
  • Review test/mocks updates
  • Post findings

@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
⚠️ Risk level: Low
📂 Priority files

  • blockchain/statebackend/deprecated.go
  • rpc/v10/storage.go
  • rpc/v9/storage.go
🏅 Score: 85
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Race Still Possible

TrieStateAtBlockNumber in the deprecated backend re-reads chain height after opening the batch and returns db.ErrKeyNotFound if it doesn't match blockNumber exactly, instead of using historical state like StateAtBlockHash does via deprecatedstate.NewHistory. This means a new block being committed between the earlier height check and this call causes the storage proof RPC to fail with a not-found error instead of using the intended block's state, rather than actually pinning to the requested block.

func (b *deprecatedStateBackend) TrieStateAtBlockNumber(
	blockNumber uint64,
) (core.StateReader, StateCloser, error) {
	if err := pruner.RequireStateRetainedByBlockNumber(
		b.database,
		b.retentionFloor,
		blockNumber,
	); err != nil {
		return nil, nil, err
	}

	//nolint:staticcheck,nolintlint // used by old state
	txn := b.database.NewIndexedBatch()
	height, err := core.GetChainHeight(txn)
	if err != nil {
		return nil, nil, err
	}
	if height != blockNumber {
		return nil, nil, db.ErrKeyNotFound
	}

	return deprecatedstate.New(txn), NoopStateCloser, nil
}

@cicr99

cicr99 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

This new solution adds a trie-capable state accessor for storage proofs:
For the new state implementation, it opens a regular StateReader from the retained state root for the requested block number, so it fixes the inconsistency properly.
However, for deprecated state, it is still not fully race-free. It checks that the deprecated trie reader is being opened while the DB still reports the same head height. If the chain already moved, it fails instead of knowingly returning the wrong state. This is still better than before, as it reduces the race window, but it's still not the optimal solution.

For full correctness we'd still need the DB snapshots solution

@cicr99
cicr99 deployed to Development September 15, 2026 17:45 — with GitHub Actions Active
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.34%. Comparing base (64e105c) to head (97dc33b).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
blockchain/statebackend/deprecated.go 76.92% 3 Missing ⚠️
blockchain/statebackend/statebackend.go 81.81% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4064      +/-   ##
==========================================
- Coverage   79.47%   79.34%   -0.13%     
==========================================
  Files         466      466              
  Lines       36015    36035      +20     
==========================================
- Hits        28623    28593      -30     
- Misses       7383     7433      +50     
  Partials        9        9              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants